/**
* 版权归 瞄你个汪 所有。
* 使用本代码应遵守相关法律法规和许可证的要求。
*/
package cn.sqlsuper.core;

import cn.sqlsuper.annotation.SuperColumn;
import cn.sqlsuper.annotation.SuperIndex;
import cn.sqlsuper.annotation.SuperTable;
import cn.sqlsuper.constants.ConstPool;
import cn.sqlsuper.mapper.TableBuilderMapper;
import cn.sqlsuper.option.SuperTableOption;
import cn.sqlsuper.interfaces.SuperSeedData;
import cn.sqlsuper.toolkit.ClassTool;
import cn.sqlsuper.toolkit.SpringBeanManager;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.type.JdbcType;
import org.apache.logging.log4j.util.Strings;

import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.sql.Date;
import java.sql.Time;
import java.sql.Timestamp;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * SuperTable 表结构同步核心类
 * @author 瞄你个汪
 * @since 2024-05-01 12:00:00
 */
@Slf4j
public class SuperTableCore {
    /**
     * SuperTable 配置选项
     */
    private SuperTableOption option;

    /**
     * MyBatisPlus表实体
     */
    private TableInfo table;

    /**
     * SQL语句构造器
     */
    private StringBuilder sqlBuilder;

    /**
     * 注解字段类型映射表
     */
    private final Map<JdbcType, String> columTypeMap = new HashMap<JdbcType, String>() {{
        put(JdbcType.BIT, "BIT");
        put(JdbcType.TINYINT, "TINYINT");
        put(JdbcType.INTEGER, "INT");
        put(JdbcType.BIGINT, "BIGINT");

        put(JdbcType.FLOAT, "FLOAT");
        put(JdbcType.DOUBLE, "DOUBLE");
        put(JdbcType.NUMERIC, "NUMERIC");
        put(JdbcType.DECIMAL, "DECIMAL");

        put(JdbcType.DATE, "DATE");
        put(JdbcType.TIME, "TIME");
        put(JdbcType.TIMESTAMP, "TIMESTAMP");

        put(JdbcType.CHAR, "CHAR");
        put(JdbcType.VARCHAR, "VARCHAR");
        put(JdbcType.LONGVARCHAR, "LONGTEXT");

        put(JdbcType.BOOLEAN, "TINYINT(1)");

        put(JdbcType.BLOB, "BLOB");
    }};

    /**
     * 默认java字段类型映射表
     */
    private final Map<Class<?>, String> classTypeMap = new HashMap<Class<?>, String>() {{
        put(Long.class, "BIGINT");
        put(long.class, "BIGINT");

        put(Integer.class, "INTEGER");
        put(int.class, "INTEGER");

        put(Short.class, "SMALLINT");
        put(short.class, "SMALLINT");

        put(Byte.class, "TINYINT");
        put(byte.class, "TINYINT");

        put(Boolean.class, "TINYINT(1)");
        put(boolean.class, "TINYINT(1)");

        put(Float.class, "FLOAT");
        put(float.class, "FLOAT");

        put(Double.class, "DOUBLE");
        put(double.class, "DOUBLE");

        put(BigDecimal.class, "DECIMAL");

        put(Date.class, "DATETIME");
        put(LocalDateTime.class, "DATETIME");
        put(LocalDate.class, "DATETIME");
        put(Time.class, "TIME");
        put(Timestamp.class, "TIMESTAMP");

        put(Character.class, "CHAR");
        put(char.class, "CHAR");

        put(String.class, "VARCHAR");
        put(Enum.class, "VARCHAR(64)");

        put(Stream.class, "BLOB");
    }};

    /**
     * 构造table用到的接口
     */
    private static final TableBuilderMapper tableBuilderMapper;

    static {
        tableBuilderMapper = SpringBeanManager.getBean(TableBuilderMapper.class);
    }

    public static void initialize() {
        SuperTableCore builder = new SuperTableCore();
        builder.option = SpringBeanManager.getBean(SuperTableOption.class);

        // 表结构初始化
        if (builder.option.isEnableInitTable()) {
            List<TableInfo> tableInfos = builder.getAllTable();
            tableInfos.forEach(builder::InitTable);
        }

        // 表种子数据初始化
        if (builder.option.isEnableInitSeed()) {
            builder.tableSeedInitialize();
        }
    }

    /**
     * 获取@SuperTable表实体信息
     * @return MyBatisPlus表实体列表
     */
    private List<TableInfo> getAllTable() {
        List<TableInfo> tableInfos = TableInfoHelper.getTableInfos();
        return tableInfos.stream().filter(x -> x.getEntityType().isAnnotationPresent(SuperTable.class)).collect(Collectors.toList());
    }

    /**
     * 构建初始化表的SQL语句
     */
    private void InitTable(TableInfo tableInfo) {
        this.table = tableInfo;
        if (!table.havePK()) throw new RuntimeException(ConstPool.SuperTable + "表[" + table.getTableName() + "]必须设置主键");
        SuperTable superTable = table.getEntityType().getAnnotation(SuperTable.class);
        if (superTable.isView()) {
            log.warn("【{}】:跳过视图 -> {}", ConstPool.SuperTable, table.getTableName());
            return;
        }

        // 同步表
        asyncTable();

        // 同步表结构
        asyncAllColumns();

        // 同步表索引
        asyncTableIndexes();
    }

    /**
     * 构建建表SQL语句
     */
    private void asyncTable() {
        final String createTableSql = "CREATE TABLE IF NOT EXISTS `{1}`(\n" +
                "`{2}` INT(11) NOT NULL,\n" +
                "PRIMARY KEY (`{2}`)\n" +
                ") ENGINE=InnoDB;";

        sqlBuilder = new StringBuilder().append(createTableSql
                .replace("{1}", table.getTableName())
                .replace("{2}", table.getKeyColumn()));

        log.info("【{}】:初始化表 {} -> {}", ConstPool.SuperTable, table.getEntityType().getName(), table.getTableName());
        execute();
    }

    /**
     * 构建初始化表所有字段的SQL语句
     */
    @SneakyThrows
    private void asyncAllColumns() {
        List<Map<String, Object>> columns = selectAllColumns(table.getTableName());
        sqlBuilder = new StringBuilder().append("ALTER TABLE `{1}`\n".replace("{1}", table.getTableName()));

        // 添加主键更新SQL
        final String columnKeyName = table.getKeyColumn();
        Field keyField = ClassTool.getField(table.getEntityType(), table.getKeyProperty());
        boolean isAdd = columns.stream().noneMatch(x -> columnKeyName.equals(x.get("Field")));
        builderInitColumnSql(table.getKeyColumn(), null, keyField, isAdd, true);

        // 添加普通字段更新SQL
        String lastColumnName = columnKeyName;
        List<TableFieldInfo> fieldList = table.getFieldList();
        for(TableFieldInfo field : fieldList) {
            String columnName = field.getColumn();
            isAdd = columns.stream().noneMatch(x -> columnName.equals(x.get("Field")));
            builderInitColumnSql(columnName, lastColumnName, field.getField(), isAdd, false);

            lastColumnName = columnName;
        }

        // 主键不一致则追加删除旧主键添加新主键的SQL语句
        if (columns.stream().noneMatch(x -> "PRI".equals(x.get("Key").toString()) && table.getKeyColumn().equals(x.get("Field").toString()))) {
            final String updatePrimarySql = "DROP PRIMARY KEY,ADD PRIMARY KEY (`{1}`) USING BTREE,\n";
            sqlBuilder.append(updatePrimarySql.replace("{1}", table.getKeyColumn()));
        }

        // 如果是增量更新表结构，删除未包含在表实体中的字段
        if (option.isEnableIncreTable()) {
            List<String> dropColumns = columns.stream()
                    .map(x -> x.get("Field").toString())
                    .filter(name -> fieldList.stream()
                            .noneMatch(y -> y.getColumn().equals(name))
                    ).collect(Collectors.toList());

            // 排除主键
            dropColumns.remove(table.getKeyColumn());

            // 如果非空，组装删除字段SQL
            if (!dropColumns.isEmpty()) {
                log.warn("【{}】:增量更新表结构,删除字段 {} -> {}", ConstPool.SuperTable, table.getEntityType().getName(), dropColumns);
                for (String column : dropColumns) {
                    sqlBuilder.append("DROP COLUMN `").append(column).append("`,\n");
                }
            }
        }

        // 追加表属性更新
        final String updateTableSql = "{1}{2}CHARACTER SET = utf8mb4, COLLATE = utf8mb4_german2_ci;\n\n";
        SuperTable superTable = table.getEntityType().getAnnotation(SuperTable.class);
        sqlBuilder.append(updateTableSql
                .replace("{1}", table.getIdType() == IdType.AUTO ? " AUTO_INCREMENT = 1, " : "")
                .replace("{2}", superTable == null || superTable.value().isEmpty() ? "" : "COMMENT = '" + superTable.value() + "', "));

        log.info("【{}】:同步表结构 {} -> {}", ConstPool.SuperTable, table.getEntityType().getName(), table.getTableName());
        execute();
    }

    /**
     * 构建单个表字段SQL语句
     */
    private void builderInitColumnSql(String columnName, String lastColumnName, Field field, boolean isAdd, boolean isPrimary) {
        final String updateColumnSql = "{1} COLUMN `{2}` {3} {4} {5}{6}{7}{8}{9},\n";
        SuperColumn superColumn = field.getAnnotation(SuperColumn.class);

        sqlBuilder.append(updateColumnSql
                .replace("{1}", isAdd ? "ADD" : "MODIFY")
                .replace("{2}", columnName)
                .replace("{3}", getColumnTypeSql(field))
                .replace("{4}", isPrimary || superColumn == null || !superColumn.nullable() ? "NOT NULL" : "NULL")
                .replace("{5}", superColumn == null || superColumn.defaultValue().isEmpty() ? "" : "DEFAULT " + superColumn.defaultValue())
                .replace("{6}", isPrimary && table.getIdType() == IdType.AUTO ? " AUTO_INCREMENT = 1 " : "")
                .replace("{7}", superColumn == null || superColumn.extra().isEmpty() ? "" : (" " + superColumn.extra() + " "))
                .replace("{8}", superColumn == null || superColumn.value().isEmpty() ? "" : (" COMMENT '" + superColumn.value() + "' "))
                .replace("{9}", isPrimary || Strings.isEmpty(lastColumnName) ? " FIRST" : String.format(" AFTER `%s`", lastColumnName)));
    }

    /**
     * 根据字段class或者数据库中对应数据格式
     * @param field 字段
     */
    private String getColumnTypeSql(Field field) {
        String columType = "";
        Class<?> type = field.getType();
        SuperColumn mapping = field.getAnnotation(SuperColumn.class);
        if (mapping != null && mapping.columType() != JdbcType.UNDEFINED)  {
            columType = columTypeMap.get(mapping.columType());
            if (Strings.isEmpty(columType))
                throw new RuntimeException("不支持的字段映射类型");
        } else {
            columType = classTypeMap.get(type);
            if (Strings.isEmpty(columType))
            {
                if (type.isEnum()) {
                    columType = classTypeMap.get(Enum.class);
                } else {
                    throw new RuntimeException("不支持的Java字段映射类型");
                }
            }
        }
        // 给字符串字段添加长度参数
        if (type.equals(String.class) && columType.startsWith(classTypeMap.get(String.class)))
            columType += "(" + (mapping == null ? 64 : mapping.length()) + ")";
        return columType;
    }

    /**
     * 构建索引更新语句
     */
    private void asyncTableIndexes() {
        SuperTable superTable = table.getEntityType().getAnnotation(SuperTable.class);
        String tableName = table.getTableName();

        if (superTable != null) {
            final String updateIndexSql = "ADD {1}INDEX `{2}`(`{3}`) USING {4},\n";
            final String dropIndexSql = "DROP INDEX `{1}`,\n";

            List<Map<String, Object>> indexes = selectAllIndexes(table.getTableName());
            List<String> oldIndexNames = indexes.stream().map(x -> x.get("Key_name").toString()).distinct().collect(Collectors.toList());
            SuperIndex[] superIndices = superTable.indexes();

            if (superIndices.length == 0 && indexes.isEmpty()) return;

            sqlBuilder.append("ALTER TABLE `{1}`\n".replace("{1}", tableName));
            List<TableFieldInfo> fieldList = table.getFieldList();

            // 获取属性名与字段名映射表
            Map<String, String> propColuMap = getPropertyToColumnMap(fieldList);
            propColuMap.put(table.getKeyProperty(), table.getKeyColumn());

            Map<String, String> indexNameMap = new HashMap<>();
            for (SuperIndex index : superIndices) {
                String indexName = index.indexName();
                indexNameMap.put(indexName, indexName.replaceAll("\\{(?i)table}", tableName));
            }

            String[] sql = new String[]{"", ""};
            for (SuperIndex index : superIndices) {
                if (index == null || index.indexFields().length == 0) continue;
                String[] strings = index.indexFields();

                // 将索引字段名映射为数据库字段名
                Set<String> propSet = propColuMap.keySet();
                for (int j = 0; j < strings.length; j++) {
                    final String regExp = "(?i)" + strings[j];
                    String propName = propSet.stream()
                            .filter(prop -> prop.matches(regExp))
                            .findFirst().orElse(null);

                    strings[j] = propColuMap.get(propName);
                    if (strings[j] == null) {
                        String format = String.format("未找到索引配置中的实体字段: %s -> %s", table.getEntityType(), strings[j]);
                        throw new RuntimeException(format);
                    }
                }

                // 替换索引名称中的表名
                String indexName = indexNameMap.get(index.indexName());
                String indexFields = String.join(",", strings);

                // 是否需要删除索引
                boolean isDrop = oldIndexNames.contains(indexName);
                if (isDrop) sql[0] += dropIndexSql.replace("{1}", indexName);
                sql[1] += updateIndexSql
                        .replace("{1}", index.isUnique() ? "UNIQUE " : "")
                        .replace("{2}", indexName)
                        .replace("{3}", indexFields)
                        .replace("{4}", "BTREE");
            }

            // 增量更新表时 删除弃用索引
            if (option.isEnableIncreTable() && superTable.increase()) {
                Collection<String> newIndexNames = indexNameMap.values();
                List<String> dropIndexNames = oldIndexNames.stream().filter(old -> !newIndexNames.contains(old))
                        .collect(Collectors.toList());

                if (!dropIndexNames.isEmpty()) {
                    log.warn("【{}】:增量更新表结构,删除索引 {} -> {}", ConstPool.SuperTable, table.getEntityType().getName(), dropIndexNames);
                    dropIndexNames.forEach(name -> sql[0] += dropIndexSql.replace("{1}", name));
                }
            }

            sqlBuilder.append(sql[0].isEmpty() ? "" : sql[0]).append(sql[1]);

            // 替换最后一个逗号
            int index = sqlBuilder.lastIndexOf(",");
            sqlBuilder.replace(index, index + 1, ";\n\n");

            log.info("【{}】:初始化表索引 {} -> {}", ConstPool.SuperTable, table.getEntityType().getName(), tableName);
            execute();
        }
    }

    /**
     * 获取类属性与数据库字段映射Map
     * @param fieldList 字段信息列表
     * @return 映射Map
     */
    private Map<String, String> getPropertyToColumnMap(List<TableFieldInfo> fieldList) {
        // TODO 添加多数据源逻辑
        Map<String, String> map = new HashMap<>();
        fieldList.forEach(e -> map.put(e.getProperty(), e.getColumn()));
        return map;
    }

    private void execute() {
        // TODO 添加多数据源逻辑
        String sql = sqlBuilder.toString();
        tableBuilderMapper.execute(sql);
        sqlBuilder = new StringBuilder();
    }

    /**
     * 初始化种子数据
     */
    @SneakyThrows
    private void tableSeedInitialize() {
        List<Class<?>> serviceClasses = ClassTool.getClassesBySuperclass(null, ServiceImpl.class);
        List<Class<?>> classes = ClassTool.getClassesByInterface(null, SuperSeedData.class);
        for(Class<?> clazz : classes) {
            // TODO 种子增量更新逻辑
            List<?> seedData = ((SuperSeedData<?>)clazz.newInstance()).getSeedData();
            if (seedData == null || seedData.isEmpty()) continue;
            Class<?> entityClazz = seedData.get(0).getClass();
            for (Class<?> serverClass : serviceClasses) {
                ServiceImpl<?, ?> serviceEntity = (ServiceImpl<?, ?>) serverClass.newInstance();
                if (entityClazz.equals(serviceEntity.getEntityClass())) {
                    ServiceImpl service = (ServiceImpl<?, ?>) SpringBeanManager.getBean(serverClass);
                    log.info("【{}】:初始化种子数据 {} -> {}条记录", ConstPool.SuperTable, entityClazz.getName(), seedData.size());
                    service.saveOrUpdateBatch(seedData);
                }
            }
        }
    }

    /**
     * 根据表名查询所有字段信息
     * @param tableName 表名
     * @return 所有字段信息
     */
    private List<Map<String, Object>> selectAllColumns(String tableName) {
        try {
            // TODO 添加多数据源逻辑
            List<Map<String, Object>> indexes = tableBuilderMapper.getAllColumns(tableName);
            return indexes == null || indexes.isEmpty() ? new ArrayList<>() : indexes;
        } catch (Exception e) {
            return new ArrayList<>();
        }
    }

    /**
     * 根据表名查询所有索引信息
     * @param tableName 表名
     * @return 所有索引信息
     */
    private List<Map<String, Object>> selectAllIndexes(String tableName) {
        try {
            // TODO 添加多数据源逻辑
            List<Map<String, Object>> indexes = tableBuilderMapper.getAllIndexes(tableName);
            return indexes == null || indexes.isEmpty() ? new ArrayList<>() : indexes;
        } catch (Exception e) {
            return new ArrayList<>();
        }
    }
}
